#!/usr/bin/env python
# coding: utf-8

# In[29]:


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.sans-serif']=['SimHei'] # 用黑体显示中文
sns.set(font='SimHei', font_scale=0.8)      # 解决Seaborn中文显示问题
import warnings
warnings.filterwarnings('ignore')

##agent表示找的旅行社，nan表示没有找
##lead_time 指订酒店的日期与到店日期的间隔天数。
##meal 是订餐类型 
##market_segment Online. TA 线上旅行社 Offline TA/TO 线下旅行社或游客 
##reserved_room 预定的房间类型，assigned_room 分配的房间类型
##booking changes 预定房间后的修改次数
##deposit_type ,non fund不退押金, no deposit 无押金 
##reservation_status 到店并且离开，或者取消
#adr表示酒店价格


# In[30]:


data = pd.read_csv('hotel_bookings.csv')
data.head()


# In[31]:


data.info() 
#company存在大量空值，agent存在大约一万条空值，children存在少量空值只有4条，country存在大约500条空值


# In[32]:


data.shape #一共有十几万行，32列数据


# # 数据清洗

# In[33]:


#children只有4条空值，用0填补
data['children'].fillna(0,inplace=True) #inplace=True在原数据上更改
data['children'].isnull().any() #查看是否还有空值


# In[34]:


#country存在大约500条空值，故用众数填充
num = data['country'].mode() #众数
data['country'].fillna('num',inplace=True)
data['country'].isnull().any()


# In[35]:


#agent出现nan表示没有找旅行社，故用0填充
data['agent'].fillna(0,inplace=True)
data['agent'].isnull().any()


# In[36]:


#company存在大量空值,故直接删除这列
data.drop('company',axis=1,inplace=True) #删除后就没有这一列了，因此只能运行一次


# In[37]:


#meal该列特征的说明是SC/Undefined 都是没有包含餐饮，所以归为一类
data['meal'] = data['meal'].replace('Undefined','SC') #用SC代替Undefined
#观察发现children agent都是浮点类型，故都改成整型
data.children = data.children.astype(int)
data['agent'] = data['agent'].astype(int)


# In[38]:


#删除入住人数为0的订单
#order_0 = data[(data['adults']==0) & (data['children']==0) & (data['babies']==0)]
data.drop(data.index[list(data['adults']==0) & (data['children']==0) & (data['babies']==0)],inplace = True )
data.info() #原数据119390条，现在119210条，删除了180条


# # 数据分析与可视化

# ### 1.分析2个酒店的预定量和取消量

# In[39]:


#饼图默认逆时针方向给label值
plt.figure(figsize=(16,10)) #创建画布
cmap = plt.get_cmap('tab20c')
plt.subplot(121)
data['hotel'].value_counts().plot.pie(labels=['City Hotel','Resort Hotel'],colors=['pink','c'],shadow=True,autopct='%.2f%%') #两个酒店的预订数
plt.axis('equal')
plt.legend()
plt.title('酒店预订数占比',fontsize=20)

plt.subplot(122)
data.groupby(['hotel','is_canceled'])['is_canceled'].count().plot(kind='pie',labels=['not canceled','canceled','not canceled','canceled'],colors=cmap(np.array([1,2,4,7])),shadow=True,autopct='%.2f%%')
plt.legend()
# 将饼图显示为正圆形,plt.axis( )
plt.axis('equal')
plt.ylabel('') #去除ylabel
plt.title('不同酒店取消和未取消分别占比',fontsize=20)


# 由图可得，City Hotel的预定量占比66%，Resort Hotel占比约33%，大约是Resort Hotel的两倍。但City Hotel的取消量大于Resort Hotel的取消量，分析是否是由于两种酒店的价格不同，或者是两种类型的酒店的押金是否退还有区别

# ##### 1.1押金方式与取消率

# In[40]:


#押金方面分析
plt.figure(figsize=(16,10))
labels1 = ['City Hotel','Resort Hotel']
labels2 = ['no Deposit','Non Refund','Refundable','no Deposit','Non Refund','Refundable']
colors1 = cmap(np.array([2,10]))
colors2 = cmap(np.array([1,7,3,13,5,6]))
plt.pie(data['hotel'].value_counts(),labels = labels1,autopct="%.2f%%",radius=0.8,labeldistance = 0.75,colors=colors1,wedgeprops = {'width':0.5},pctdistance=0.6,shadow=True)
plt.pie(data.groupby('hotel')['deposit_type'].value_counts(),labels=labels2,autopct="%.2f%%",radius=1.4,labeldistance = 0.95,wedgeprops = {'width':0.6},colors=colors2,pctdistance=0.85,shadow=True)
# 将饼图显示为正圆形,plt.axis( )
plt.axis('equal')
plt.legend()
plt.title('各酒店在押金方面的占比图',fontsize=20)


# 从图中可以看出，City Hotel不需要支付押金的比例低于Resort Hotel，但是City Hotel的取消量却大于Resort Hotel的取消量。

# In[41]:


data.groupby('deposit_type')['is_canceled'].value_counts()


# In[42]:


#分析is_canceled和deposit_type之间的关系
plt.figure(figsize=(16,10))
labels3 = ['No Deposit','Non Refund','Refundable']
labels4 = ['not canceled','canceled','canceled','not canceled','not canceled','canceled']
colors3 = cmap(np.array([2,10,15]))
colors4 = cmap(np.array([1,7,4,13,5,6]))
plt.pie(data['deposit_type'].value_counts(),autopct="%.2f%%",labels = labels3,radius=0.8,labeldistance = 0.75,colors=colors3,wedgeprops = {'width':0.5},pctdistance=0.6,shadow=True)
plt.pie(data.groupby('deposit_type')['is_canceled'].value_counts(),labels=labels4,autopct="%.2f%%",radius=1.4,labeldistance = 0.95,wedgeprops = {'width':0.6},colors=colors4,pctdistance=0.85,shadow=True)
# 将饼图显示为正圆形,plt.axis( )
plt.axis('equal')
plt.legend()
plt.title('押金类型和是否取消',fontsize=20)


# 图中可知，无押金不取消的占比最大.但不退押金中取消的比例几乎占全部，这是不符号现实的，所以将无押金的订单取消0改为1，1改为0

# In[43]:


data[data.deposit_type == 'Non Refund'].is_canceled.value_counts()


# In[44]:


#将无押金的订单取消0改为1，1改为0
data[data['deposit_type']=='Non Refund']['is_canceled'] = data[data['deposit_type']=='Non Refund']['is_canceled'].apply(lambda x:0 if x==1 else 1)
data[data['deposit_type']=='Non Refund']['is_canceled'].value_counts()
#因此我们可以得知，需要押金的取消率几乎为0，虽然无需押金的取消率相对较大，但这是符合常理的


# ##### 2.2价格方面与取消率

# In[45]:


data['adr'].describe()
#可以看出最小值为负数，最大值达到5400，这显然是不符合常理的。这里用距离平均值大于十个标准差筛选掉极其不合理的数据，这里也就是大于600，以及小于等于0的数据，将异常值删除掉。


# In[46]:


data_t = data[(data['adr']<600) & (data['adr']>1)]
data_t.head()


# In[47]:


#sns.set(palette="muted", color_codes=True)
plt.figure(figsize=(12,8))
sns.distplot(data_t['adr'])
plt.xlabel('每晚平均价格')
plt.title('平均每晚价格密度',fontsize=20)
plt.show()


# 可以看出最大密度为平均每晚价格100的，呈现左偏分布

# In[48]:


#data_t_mean_adr = data_t.groupby(['hotel','arrival_date_month'])['adr'].mean()
plt.figure(figsize=(10,6))
plt.xticks(np.arange(12)) #添加数据标签
data_t[data_t['hotel']=='Resort Hotel'].groupby('arrival_date_month')['adr'].mean().plot()
data_t[data_t['hotel']=='City Hotel'].groupby('arrival_date_month')['adr'].mean().plot()
plt.title('各酒店每月平均价格对比',fontsize=20)
plt.legend(['Resort Hotel','City Hotel'],fontsize=12)
plt.xlabel('月份',fontsize=12)
plt.ylabel('均价',fontsize=12)


# Resort Hotel的每月均价始终小于City Hotel，但在5月突然呈现很快的上升趋势，并在6月过后超过City Hotel，在8月价格达到最高。推测是由于订单数增多导致价格上涨

# ##### 1.3客源地与预订取消率

# In[49]:


data_t.head()


# In[86]:


#palette设置调色板
country_book=data_t['country'].value_counts()[:10]
country_cancel=data_t[(data_t['country'].isin (country_book.index)) & (data_t.is_canceled==1)]['country'].value_counts()
plt.figure(figsize=(15,8))
sns.countplot(x='country'
              ,data=data_t[data_t.country.isin (country_book.index)]
              ,hue='is_canceled'
              ,palette=sns.color_palette('Set2',2)
             )
plt.title('顾客来源与取消量',fontsize=20)


# 可以发现客源主要为欧洲国家，符合欧洲游客偏爱夏季出游的特点，需要重点关注来自英国(BRT)等取消率高的主要客源

# ##### 1.4房间类型与预定取消量

# In[87]:


plt.figure(figsize=(12,8))
sns.countplot(x='assigned_room_type',
             data=data_t,
             hue='is_canceled',
              palette=sns.color_palette('Set3',2)
             )
plt.title('房间类型与取消量',fontsize=20)


# A房型的取消量最高，A和G房型的取消量的柱子都高于未取消的一半，取消率较高

# ### 2.时间维度

# In[28]:


#一共有15,16,17三年的数据
month = ["January", "February", "March", "April", "May", "June", "July", "August","September", "October", "November", "December"]
#设置默认排序
data_t['arrival_date_month'] = pd.Categorical(data_t['arrival_date_month'],month)
data_15 = data_t[data_t.arrival_date_year == 2015]
data_16 = data_t[data_t.arrival_date_year == 2016]
data_17 = data_t[data_t.arrival_date_year == 2017]

plt.figure(figsize=(12,45))
plt.subplot(5,1,1)
data_15.groupby('arrival_date_month')['adr'].mean().plot(kind='bar',color='red',alpha=0.6)
plt.legend()
plt.title('2015年平均价格变化图',fontsize=12)
plt.xlabel('月份')

plt.subplot(5,1,2)
data_16.groupby('arrival_date_month')['adr'].mean().plot(kind='bar',color='blue',alpha=0.6)
plt.legend()
plt.title('2016年平均价格变化图',fontsize=12)
plt.xlabel('月份')

plt.subplot(5,1,3)
data_17.groupby('arrival_date_month')['adr'].mean().plot(kind='bar',color='green',alpha=0.6)
plt.legend()
plt.title('2017年平均价格变化图',fontsize=12)
plt.xlabel('月份')

plt.subplot(5,1,4)
sns.barplot(x = 'arrival_date_month',y = 'adr',hue ='arrival_date_year',estimator = np.mean,data = data_t,ci=0)
plt.title('每年各月平均价格对比',fontsize=12)
plt.xlabel('月份')
plt.ylabel('平均价格')

plt.subplot(5,1,5)
plt.xticks(np.arange(12)) #添加数据标签
data_15.groupby('arrival_date_month').adr.mean().plot(marker='*')
data_16.groupby('arrival_date_month').adr.mean().plot(marker='*')
data_17.groupby('arrival_date_month').adr.mean().plot(marker='*')
plt.title('每年各月平均价格对比',fontsize=12)
plt.xlabel('月份')
plt.legend(['2015年','2016年','2017年'])


# ##我们可以发现每年均价最高的都是8月，最低的是1月，八月之前呈现上升趋势，八月之后呈现下降趋势，也就是说夏季的价格高于冬季，猜测价格与季节有关。
# ##2015年一整年每月的均价都处于三年中的最低（除了2015，2017年不完整的数据），可以推测随着人们消费水平的提高，酒店的价格也在上涨
# ##每月的价格变化，猜测可能和入住的人数有关，当月入住的人多时，酒店价格也会随之上涨

# In[23]:


#每月订单分析
plt.figure(figsize=(16,6))
plt.xticks(np.arange(12)) #添加数据标签
plt.tick_params(labelsize = 12)
data_t['订单数'] = 1
plt.subplot(1,2,1)
sns.lineplot(x = 'arrival_date_month', y = '订单数', data = data_t ,hue = 'hotel',estimator = np.sum , ci = 0)
plt.title('各类型酒店每月订单数',fontsize = 20)
plt.subplot(1,2,2)
sns.barplot(x = 'arrival_date_month', y = '订单数', data = data_t ,hue = 'hotel',estimator = np.sum , ci = 0)
plt.title('各类型酒店每月订单数',fontsize = 20)


# #可以看出，两个酒店的订单数每月的变化趋势大致相同，Resort hotel每月的订单数都高于City hotel。
# #说明订单数是随着季节变化的，夏季入住的人多于冬季，推测是由于7，8月份是旅游旺季，1月左右是过年的日子，入住酒店的人降到最低

# ### 3.新老顾客

# In[24]:


plt.figure(figsize=(14,10))
plt.subplot(1,2,1)
data_t['is_repeated_guest'].value_counts().plot.pie(labels=(['新顾客','老顾客']),colors=cmap(np.array([1,5])),autopct='%.2f%%')
plt.axis('equal') #将饼图显示为正圆形
plt.legend()
plt.title('新老顾客占比',fontsize=20)
plt.ylabel('')

plt.subplot(1,2,2)
label5 = ['新顾客未取消','新顾客取消','老顾客未取消','老顾客取消']
data_t.groupby('is_repeated_guest')['is_canceled'].value_counts().plot.pie(labels=label5,colors=cmap(np.array([1,4,8,15])),autopct='%.2f%%')
plt.axis('equal')
plt.legend()
plt.title('新老顾客是否取消占比',fontsize=20)
plt.ylabel('')


# #图一可以看出，大多数客户都是新顾客，说明是大多数客户都只是暂住，可能是出差或者旅游，也可能是因为酒店的环境设备地理位置等不符合顾客的期望，因此老客户很少。酒店在招揽新顾客时也应该关注客户的留存问题。
# #图二可以看出，新老顾客的未取消占比都大于取消占比，但新顾客的取消率要大于老顾客的取消率，说明老顾客对酒店的信任度较高

# # 建模预测订单是否会取消

# In[4]:


cancel_corr = data.corr()['is_canceled'] #其他int型列与is_canceled的相关系数
cancel_corr.abs().sort_values(ascending = False)  #取绝对值按降序排


# In[3]:


from sklearn.model_selection import train_test_split, KFold, cross_validate, cross_val_score
from sklearn.preprocessing import StandardScaler,LabelEncoder, OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from xgboost import XGBClassifier
import eli5


# In[4]:


#数值类型的特征和类别型特征
num_features = ["lead_time","total_of_special_requests","required_car_parking_spaces", 
                 "previous_cancellations","is_repeated_guest","adults","previous_bookings_not_canceled","agent",
                "adr","babies","stays_in_weekend_nights","arrival_date_week_number","arrival_date_day_of_month",
                "children","stays_in_week_nights"]
cat_features = ["hotel","arrival_date_month","meal","market_segment",
                "distribution_channel","reserved_room_type","deposit_type","customer_type"]
features = num_features + cat_features

#x为特征值，y为预测值
x = data.drop(['is_canceled'],axis=1)[features]
y = data['is_canceled'] 

#预处理数值特征
#定义转换器
#自定义值填补，fii_value自定义，默认值0
num_transformer = SimpleImputer(strategy='constant')

#类别型特征的预处理，缺失值填补Unknown，其余的用独热编码转换
#定义转换器。使用管道编写
cat_transformer = Pipeline(steps=[("imputer", SimpleImputer(strategy="constant", fill_value="Unknown")),
                                  ("onehot", OneHotEncoder(handle_unknown='ignore'))])

# 数值和分类特征的束预处理
#用ColumnTransformer定义转换规则
#ColumnTransformer必须指定一个转换器列表，每个转换器是一个三元素元组（用于定义转换器的名称，要应用的转换，要应用于其的列索引）
preprocessor = ColumnTransformer(transformers=[("num", num_transformer, num_features),("cat", cat_transformer, cat_features)])


# In[5]:


#要测试的模型
base_models = [
    ("DT", DecisionTreeClassifier(random_state=42)),
    ("RF", RandomForestClassifier(random_state=42)),
    ("LR", LogisticRegression(random_state=42)),
    ("XGB", XGBClassifier(random_state=42))
]
#用k折交叉验证来选择最优模型
split = KFold(n_splits=5,shuffle=True, random_state=42)
for name,model in base_models:
    pipe = Pipeline(steps=[('preprocessor',preprocessor),('model',model)])
    #cross_val_score(估计器，x,y,几次,调用的方法，n_jobs=-1表示cpu全部同时工作)
    cv_res = cross_val_score(pipe,x,y,cv=split,scoring='accuracy')
    mean_score = round(np.mean(cv_res),5)
    print(f"{name} cross validation accuarcy score: {mean_score} ")


# In[6]:


#可以看出RF模型最好
#交叉验证来确定模型的参数
#森林里数目数量设置100，每个决策树最大特征数量比例设置0.4
rf_new = RandomForestClassifier(n_estimators=100,max_features=0.4,random_state=42)
split_new = KFold(n_splits=5,shuffle=True, random_state=42)
pipe_new = Pipeline(steps = [('preprocessor',preprocessor),('model',rf_new)])
cv_res_new = cross_val_score(pipe_new,x,y,cv=split_new,scoring='accuracy')
mean_score_new = round(np.mean(cv_res_new),5) #五次验证结果取平均值
print(f"RandomForest model cross validation accuarcy score: {mean_score_new} ")
#多次调整数目数量和最大特征数量，发现准确率都差不多


# In[8]:


pipe_new.fit(x,y)
#获取独热编码中的列名
onehot_columns = list(
    pipe_new.named_steps['preprocessor'].
    named_transformers_['cat'].
    named_steps['onehot'].
    get_feature_names(input_features=cat_features)
)
feat_imp_list = num_features + onehot_columns #新列名
#获取前五个重要特征
feat_imp_df = eli5.formatters.as_dataframe.explain_weights_df(
    pipe_new.named_steps["model"],
    feature_names=feat_imp_list
)
feat_imp_df.head()


# # 对影响预测结果最大的三个特征进行分析

# #### 1.分析lead_time

# In[20]:


#可以看出前2个特征分别是lead_time, deposit_type
lead_cancel  = data.groupby('lead_time')['is_canceled'].describe()
#lead_cancel_data.head()
plt.figure(figsize=(16,12))
plt.subplot(2,2,1)
lead_cancel10 = lead_cancel[lead_cancel['count']>10]['mean']
plt.scatter(lead_cancel10.index , lead_cancel10.values)
plt.xlabel('预订时间和到店时间间隔',fontsize=12)
plt.ylabel('取消率',fontsize=12)
plt.title('订单量大于10的取消率',fontsize=20)

plt.subplot(2,2,2)
lead_cancel50 = lead_cancel[lead_cancel['count']>50]['mean']
plt.scatter(lead_cancel50.index , lead_cancel50.values)
plt.xlabel('预订时间和到店时间间隔',fontsize=12)
plt.ylabel('取消率',fontsize=12)
plt.title('订单量大于50的取消率',fontsize=20)

plt.subplot(2,2,3)
lead_cancel200 = lead_cancel[lead_cancel['count']>200]['mean']
plt.scatter(lead_cancel200.index , lead_cancel200.values)
plt.xlabel('预订时间和到店时间间隔',fontsize=12)
plt.ylabel('取消率',fontsize=12)
plt.title('订单量大于200的取消率',fontsize=20)

plt.subplot(2,2,4)
lead_cancel1000 = lead_cancel[lead_cancel['count']>1000]['mean']
plt.scatter(lead_cancel1000.index , lead_cancel1000.values)
plt.xlabel('预订时间和到店时间间隔',fontsize=12)
plt.ylabel('取消率',fontsize=12)
plt.title('订单量大于1000的取消率',fontsize=20)


# 可以看出取消率与时间间隔呈正相关，当时间间隔很小时，取消率几乎为0，也就是说最近订单的取消率更低一些。少数点的时间间隔很大，但取消率很低，可能是因为样本数量较少，不具有参考性

# #### 2.分析deposit_type

# In[21]:


#权重占比第二高的是deposit_type_Non Refund，第三高的是deposit_type_No Deposit，因此我们直接对deposit_type进行分析
deposit_cancel = data.groupby('deposit_type')['is_canceled'].describe()
deposit_cancel


# In[27]:


plt.figure(figsize=(12,8))
deposit_cancel['mean'].plot.bar(color='blue',alpha=0.4)
plt.legend()
plt.xlabel('押金类型',fontsize=10)
plt.ylabel('取消率',fontsize=10)
plt.title('押金类型和取消率的关系',fontsize=20)


# (Non Refund)这一类型的取消预订率高达99%，可以减少这一类型的押金方式以减少客户取消率¶

# 总结分析：
# #City Hotel的预定量和取消率都远高于Resort Hotel，该酒店应对客户展开调研，深入了解导致客户最终放弃预订的因素从而降低客户的取消率
# #预订时间与到店时间的间隔，是否有押金和押金是否退还，这两个特征是影响顾客是否取消预订最重要的原因，间隔时间过长，也许顾客会取消原计划因此取消退订，不退押金是顾客最不能接受的，因此取消率极高，符合常理，酒店应优化这种方式，如返还50%的押金，或者可以直接取消这一种方式，从而提高入住率。
# #按理来说，没有押金的取消率应该低于退还押金的取消率，但没有押金的取消率却高一些，说明顾客更在意酒店的地理位置，服务等
# #除此之外，影响顾客是否取消的特征还有酒店的价格，从之前的分析可以看出，夏季的价格高于冬季，因为7，8月份正值旅游旺季，所以预订的人会比较多，酒店价格随之增高，部分预订的顾客发现酒店价格上涨就会取消订单，因此取消订单也会增多
# #酒店应利用好每年7、 8月的旅游旺季，可以在保证服务质量的同时适当提高价格获取更多利润，在淡季（冬季）的时候进行优惠活动，如圣诞大促和新年活动，减少酒店空房率
# #酒店97%都是新顾客，其中有的是因为旅游或出差等原因暂住，应该也有部分顾客是因为酒店的服务、环境、地理位置等不满意而选择不再来消费。因此酒店的留存率并不好，酒店经营者应从多方面进行改善来提高留存率
# #酒店应当从押金、价格、环境等方面做出改进，价格波动不要突然浮动过大，这样会导致部分预订的客户取消预订。除了外界因素（比如预订时间与到达时间间隔过长，到达时间刚好有突然情况而到不了等），消费者预订一个酒店往往会先考虑价格方面（比如押金是否退还，住一晚上大概是多少钱），然后是酒店环境、服务、地理位置是否符合预期，因此酒店可以在消费者结束订单后，发起一个邀请评价的活动，根据消费者的体验来改进不足。

# In[ ]:





# In[ ]:





# In[ ]:





# In[ ]:




